<?php
/**
 * @file CSV_module
 *  CSVを扱うクラス群.
 * @author nagai
 */

/**
 * CSVデータのパース委任クラス.
 * @package CSV
 * @author nagai
 * @version $Id: csv_parse.php 29 2009-11-26 08:23:19Z tzr250rsp $
 */
class csv_parse {

  /**
   * フィールド区切り文字.
   */
  private $_excape = "\"";

  /**
   * escapeする文字をセット
   * @param string $char
   */
  public function setEscape($char = "\"") {
    $this->_excape = $char;
  }

  /**
   * escapeする文字を取得
   * @return string
   */
  public function getEscape() {
    return $this->_excape;
  }

  /*
   * パース前データのセッタ
   */
  public function setData($d) { $this->data_string_ = $d; }

  /*
   * パース前データのゲッタ
   */
  private function getData() { return $this->data_string_; }

  /*
   * パース後データのセッタ
   */
  public function setParsedData($d) { $this->data_array_ = $d; }

  /*
   * パース後データのゲッタ
   */
  public function getParsedData() { return $this->data_array_; }

  /*
   * パース
   */
  public function parse() {

    // CSVワークオブジェクト生成&データセット
    // 以降はCSVワークオブジェクトに実処理を委任
    $csv_wk = new csvParseWork();
    $csv_wk->setEscape($this->getEscape());
    $csv_wk->setData($this->getData());

    // パース後データを自分に保持
    // getParsedData() でパース後データを取得する為に.
    $this->setParsedData($csv_wk->getParsedData());
  }

  /**
   * パース前データ
   */
  private $data_string_;

  /**
   * パース後データ
   */
  private $data_array_ = array();

}
// end of csvParse class

/**
 * CSVデータのワーククラス.
 * 状態(モード)によって、パースオブジェクトをコール.
 * @package CSV
 * @author nagai
 * @version $Header: $
 * @see lib_Csv_Parse
 */
class csvParseWork {
  /**
   * フィールド区切り文字.
   * @var string
   */
  private $_excape = "\"";

  /**
   * コンストラクタ
   */
  public function __construct() {
    $this->init();
  }

  /**
   * escape文字をセットします.
   * @param string $char
   */
  public function setEscape($char = "\"") {
    $this->_excape = $char;
  }

  /**
   * escape文字を返します.
   * @return string
   */
  public function getEscape() {
    return $this->_excape;
  }

  /**
   * 初期動作.
   * @see __construct()
   */
  public function init() {
    // 状態遷移オブジェクト生成
    $this->objs_["set_normal"]  = new csvParseSetNormal();    // 通常モード
    $this->objs_["set_crlf"]    = new csvParseSetCrlf();      // 改行モード
    $this->objs_["set_esp"]     = new csvParseSetEsp();      // エスケープモード
    $this->objs_["set_esp_chk"] = new csvParseSetEspChk();  // エスケープチェックモード

    // 初期モードは通常モードをセット
    $this->setNormalMode();

    // XXX ファイル末尾の改行対策.
    $this->empty_flg_ = true;
  }

  /**
   * データがあるかどうか判断.
   * @note
   * emptyメソッドなので「なければtrue」
   * @return bool
   */
  public function isEmpty() { return $this->empty_flg_; }

  /**
   * 状態(モード)のセッタ.
   * @param string
   */
  public function setMode($s) { $this->mode_ = $s; }

  /**
   * 状態(モード)のゲッタ.
   * @return string
   */
  public function getMode() { return $this->mode_; }

  /**
   * 通常モードのセッタ.
   */
  public function setNormalMode() { $this->setMode("set_normal"); }

  /**
   * 改行モードのセッタ.
   */
  public function setCrlfMode() { $this->setMode("set_crlf"); }

  /**
   * エスケープモードのセッタ.
   */
  public function setEspMode() { $this->setMode("set_esp"); }

  /**
   * エスケープチェックモードのセッタ.
   */
  public function setEspChkMode() { $this->setMode("set_esp_chk"); }

  /**
   * パース前データのセッタ.
   * @param string
   */
  public function setData($d) {
    $this->data_string_ = $d;
    $this->setLength();    // データ長を保持
    $this->parse();          // パース実行
  }

  /**
   * パース前データのゲッタ.
   * @param string $p
   * @return string
   */
  public function getData($p="") {
    if (0 < strlen($p)) {
      return $this->data_string_[$p];
    } else {
      return $this->data_string_;
    }
  }

  /**
   * パース後データのゲッタ.
   * @return array
   */
  public function getParsedData() { return $this->data_array_; }

  /**
   * 状態遷移オブジェクトのインスタンスを返す.
   * @return Object
   */
  public function getParseObj() {  return $this->objs_[$this->getMode()];  }

  /**
   * 1文字ゲッタ.
   * @return int
   */
  public function getChar() { return $this->getData($this->pos_); }

  /**
   * データ長を保持.
   */
  public function setLength() { $this->len_ = strlen($this->getData()); }

  /**
   * データ長を返す.
   * @return int
   */
  public function getLength() { return $this->len_; }

  /**
   * 1文字をストック.
   */
  public function pushChar() {
    $this->stac_ .= $this->getChar();
    // データが詰まれた事を保持するため、falseをセット
    $this->empty_flg_ = false;
  }
  /**
   * 1フィールドの文字列をストック、これが溜まっていくと1レコード分のデータになる.
   */
  public function pushCol() {
    //print "push col stac is " . $this->stac_ . "\n";
    $this->col_[] = $this->stac_;  // 1フィールド分の文字列を突っ込む.
    $this->stac_  = "";
  }

  /**
   * パース後データを蓄積.
   */
  public function pushDatas() {

    //print "in push data " ."\n";

    $this->data_array_[] = $this->col_;  // 1レコード分のデータを突っ込む.
    $this->col_ = array();
    // 行データが確定したので、データの状態をemptyに戻す
    $this->empty_flg_ = true;
  }

  /**
   * バッファクリア.
   */
  public function bufClear() {
    $this->pushCol();
    $this->pushDatas();
  }

  /**
   * Iterate用のfetchメソッド.
   * 文字ポインタが、データ長以上なら偽を返す
   * @return bool
   */
  public function fetch() {

    // 文字ポインタのインクリメント
    $this->incPos();

    // Iterate終了判定.
    if ($this->getLength() <= $this->pos_) {
      return false;
    }

    return true;

  }

  /**
   * 文字ポインタを一つ進める.
   */
  public function incPos() { $this->pos_++ ; }

  /**
   * 文字ポインタを一つ戻す.
   */
  public function decPos() { $this->pos_-- ; }

  /**
   * パースを実行.
   */
  public function parse() {

    // データ長だけぶん回してパース
    while ($this->fetch()) {

      //print "before mode:" . $this->getMode() . "\n";
      //print "char:" . $this->getChar() . "\n";

      $this->getParseObj()->parse($this);

      //print "after  mode:" . $this->getMode() . "\n";
    }

    // XXX データが詰まれているときだけバッファクリア
    // XXX この判定が無いと、改行で終わってるときに不具合発生.
    if (! ($this->isEmpty()) ) {
      $this->bufClear();
    }
  }

  /**
   * 状態(モード)
   */
  private $mode_;

  /**
   * 状態遷移オブジェクト格納配列.
   */
  private $objs_ = array();

  /**
   * パース前データ
   */
  private $data_string_;

  /**
   * パース後データ
   *   array
   */
  private $data_array_ = array();

  /**
   * スタックデータ(1フィールド分の文字列を保持)
   */
  private $stac_;

  /**
   * 行スタックデータ(1レコード分のデータを保持)
   */
  private $col_;

  /**
   * 文字ポインタ
   */
  private $pos_ = -1;

  /**
   * データ長
   */
  private $len_ = -1;

  /**
   * Emptyフラグ
   */
  private $empty_flg_;

}
// end of csvParseWork class

//---------------XXX以下、オートマトン クラス(状態遷移用)
/**
 * @defgroup CSV_AutomuttonGroup CSVの状態遷移クラス郡.
 * @{
 */
/**
 * オートマトン.通常モード.
 * @brief class csvParseSetNormal in CSV_AutomuttonGroup
 *@n   最も基本的な状態を扱う、以下の挙動を行う.
 *@n     ','     cellの区切りを意味する文字と判断する.
 *@n     '"'     エスケープモードへのモード切替を行う
 *@n     '\r'    改行だが、CR-LFの可能性を考え、CRLFモードに切り替える.
 *@n     '\n'    改行。一行のデータの終わりと判断する.
 *@n     any     通常のデータとみなす
 * @package CSV
 * @author nagai
 * @version $Header: $
 * @see csvParseWork
 */
class csvParseSetNormal {

  /**
   * 通常モードのパーサ
   * @param csvParseWork $wk
   */
  public function parse($wk) {

    switch ($wk->getChar()) {
      // cell区切り.
      case ",":
        $wk->pushCol();        // 1フィールド分の文字列をストック
        $wk->setNormalMode();  // 通常モードへ
        break;
        // エスケープモード切替え
      case $wk->getEscape():
        $wk->setEspMode();    // エスケープモードへ
        break;
        // CRLFモード切替え
      case "\r":
        $wk->setCrlfMode();    // CRLFモードへ
        break;
        // レコード区切り.
      case "\n":
        $wk->bufClear();        // 行データ確定＆バッファクリア
        $wk->setNormalMode();  // 通常モードへ
        break;
        // any(通常データ)
      default:
        $wk->pushChar();        // 1文字をストック
        $wk->setNormalMode();  // 通常モードへ
    }

  }

}
// end of class

/**
 * オートマトン.改行モード.
 * @brief class csvParseSetCrlf in CSV_AutomuttonGroup
 *@n CR-LFを想定しているモード。以下の挙動を行う.
 *@n '\n'    '\r\n'であったため、ここで一行のデータの終わりと判断
 *@n any     '\r\n'ではなかったため、現在のデータを通常モードで再処理
 * @package CSV
 * @author nagai
 * @version $Header: $
 * @see csvParseWork
 */
class csvParseSetCrlf {

  /**
   * 改行モードのパーサ.
   * @param csvParseWork $wk
   */
  public function parse($wk) {

    switch ($wk->getChar()) {
      // レコード区切り(\r\nだから.)
      case "\n":
        $wk->bufClear();        // 行データ確定＆バッファクリア
        $wk->setNormalMode();  // 通常モードへ
        break;
        // any(通常モードに差し戻し)
      default:
        $wk->bufClear();        // (\rだったから)行データ確定＆バッファクリア
        $wk->decPos();          // 通常モード再処理の為、ポインタ戻す
        $wk->setNormalMode();  // 通常モードへ
    }

  }

}
// end of class

/**
 * オートマトン.エスケープモード.
 * @brief class csvParseSetEsp in CSV_AutomuttonGroup
 *@n '"'や','を入れるためのモード。','はただのデータとして扱う.
 *@n 以下の挙動を行う.
 *@n   '"'     モード切替かデータかわからないため、エスケープチェックモードへ切り替え
 *@n   any     通常のデータとみなす
 * @package CSV
 * @author nagai
 * @version $Header: $
 * @see csvParseWork
 */
class csvParseSetEsp {

  /**
   * エスケープモードのパーサ
   * @param csvParseWork $wk
   */
  public function parse($wk) {

    switch ($wk->getChar()) {
      // エスケープチェックモードへ切り替え
      case $wk->getEscape():
        $wk->setEspChkMode();  // エスケープチェックモードへ
        break;
        // any(通常データ)
      default:
        $wk->pushChar();          // 1文字をストック
        $wk->setEspMode();      // エスケープモードへ
    }
  }
}
// end of class

/**
 * オートマトン.エスケープチェックモード.
 * @brief class csvParseSetEspChk in CSV_AutomuttonGroup
 *  *@n   エスケープモード中に出てくる'"'を処理するためのモード
 *@n   以下の挙動を行う.
 *@n     '"'     '""'となっているため、データに'"'を追加、モードはエスケープ
 *@n     any     エスケープモードから通常モードへの切り替え指示とみなす.
 *@n             モードを通常に切り替え、現在のデータを通常モードで再処理
 * @package CSV
 * @author nagai
 * @version $Header: $
 * @see csvParseWork
 */
class csvParseSetEspChk {

  /**
   * エスケープチェックモードのパーサ.
   *
   * @param csvParseWork $wk
   */
  public function parse($wk) {

    switch ($wk->getChar()) {
      // データとしての'"'をストック
      case $wk->getEscape():
        $wk->pushChar();              // 1文字をストック
        $wk->setEspMode();          // エスケープモードへ
        break;
        // any 通常モードへの切り替え
      default:
        $wk->decPos();                // 通常モード再処理の為、ポインタ戻す
        $wk->setNormalMode();        // 通常モードへ
    }

  }

}
// end of class
/** @}end of CSV_AutomuttonGroup */

?>